﻿// ============================================================================
//  _ __   ___  ___  ___ _ __ ___  _   _
// | '_ \ / _ \/ __|/ _ \ '_ ` _ \| | | |
// | | | |  __/\__ \  __/ | | | | | |_| |
// |_| |_|\___||___/\___|_| |_| |_|\__,_|
//
// NES Emulator by daxnet, 2024
// MIT License
// ============================================================================

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using System.Threading;
using NesEmu.Core.Mappers;

namespace NesEmu.Core;

/// <summary>
/// Represents an NES emulator.
/// </summary>
public class Emulator
{
    #region Public Events

    public event EventHandler<DrawFrameEventArgs>? DrawFrame;

    public event EventHandler<CpuStepEventArgs>? CpuStepped;

    #endregion Public Events

    #region Internal Methods

    internal void OnDrawFrame(byte[] frameBitmapData)
    {
        DrawFrame?.Invoke(this, new DrawFrameEventArgs(frameBitmapData)
        {
            A = Cpu.A,
            X = Cpu.X,
            Y = Cpu.Y,
            SP = Cpu.SP,
            PC = Cpu.PC,
            CpuFlags = Cpu.Flag,
            PpuAddress = Ppu.PpuAddress.Value,
            PpuControl = Ppu.PpuControl.Value,
            PpuMask = Ppu.PpuMask.Value,
            PpuScroll = Ppu.PpuScroll.Value,
            PpuStatus = Ppu.PpuStatus.Value
        });
        _frameflip = !_frameflip;
    }

    #endregion Internal Methods

    #region Private Methods

    private void ExecuteStep()
    {
        var orig = _frameflip;
        while (orig == _frameflip)
        {
            if (_stop) break;

            var cpuCycles = Cpu.Step();
            for (var i = 0; i < cpuCycles * 3; i++)
            {
                // PPU runs 3x faster than CPU
                Ppu.Step();
            }
        }
    }

    #endregion Private Methods

    #region Private Fields

    // ReSharper disable once InconsistentNaming
    private static readonly List<MapperRegistry> _registeredMapperTypes =
    [
        new MapperRegistry(0x00, "NROM", e => new NRomMapper(e)),
        new MapperRegistry(0x02, "UxROM", e => new UxRomMapper(e))
    ];

    private readonly Lazy<Bus> _bus;

    private readonly Lazy<Controller> _controller;

    private readonly Lazy<Cpu> _cpu;

    private readonly Lazy<Ppu> _ppu;

    private bool _frameflip;

    private Mapper? _mapper;

    private bool _stop = true;

    #endregion Private Fields

    #region Public Constructors

    public Emulator() : this(e => new Cpu(e), e => new Bus(e), e => new Ppu(e), () => new Controller())
    {
    }

    public Emulator(Func<Emulator, Cpu> cpuResolver, Func<Emulator, Bus> busResolver)
        : this(cpuResolver, busResolver, e => new Ppu(e), () => new Controller())
    {
    }

    public Emulator(Func<Emulator, Cpu> cpuResolver, Func<Emulator, Bus> busResolver, Func<Emulator, Ppu> ppuResolver,
        Func<Controller> controllerResolver)
    {
        _cpu = new Lazy<Cpu>(() => cpuResolver(this));
        _bus = new Lazy<Bus>(() => busResolver(this));
        _ppu = new Lazy<Ppu>(() => ppuResolver(this));
        _controller = new Lazy<Controller>(controllerResolver);

        Cpu.Stepped += OnCpuStepped;
    }

    private void OnCpuStepped(object? sender, CpuStepEventArgs e)
    {
        CpuStepped?.Invoke(this, e);
    }

    #endregion Public Constructors

    #region Public Properties

    public Bus Bus => _bus.Value;

    public Controller Controller => _controller.Value;

    public Cpu Cpu => _cpu.Value;

    public Cartridge? InstalledCartridge { get; private set; }

    public Mapper Mapper => _mapper ??
                            throw new InvalidOperationException(
                                "Mapper was not initialized, make sure the cartridge is plugged in.");

    public Ppu Ppu => _ppu.Value;

    public bool Running => !_stop;

    #endregion Public Properties

    #region Public Methods

    public static IEnumerable<MapperRegistry> GetRegisteredMapperInfo() => _registeredMapperTypes;

    /// <summary>
    ///     Dumps the disassemble code for the current instruction.
    /// </summary>
    /// <returns>The disassembled code.</returns>
    /// <exception cref="InvalidOperationException">Throws if there is no cartridge installed.</exception>
    public string Disassemble()
    {
        if (InstalledCartridge is null) throw new InvalidOperationException("Cartridge is not installed.");

        Cpu.Reset();
        Bus.Reset();

        var nmiVector = Bus.ReadWord(0xfffa);
        var irqVector = Bus.ReadWord(0xfffe);
        var sb = new StringBuilder();
        sb.AppendLine($"; NMI vector:   ${nmiVector:x4}");
        sb.AppendLine($"; RESET vector: ${Cpu.PC:x4}");
        sb.AppendLine($"; IRQ vector:   ${irqVector:x4}");
        sb.AppendLine();
        var baseAddress = 0x8000 + InstalledCartridge.PrgRom.Length % 0x8000;
        if (Cpu.PC < baseAddress) baseAddress = 0x8000;

        var programCode = InstalledCartridge.PrgRom;
        var pc = 0;
        byte? prevCode = default;
        while (pc < programCode.Length)
        {
            var opcode = programCode[pc];
            if (opcode == prevCode && opcode == 0) break;

            prevCode = opcode;
            var instructionSize = Cpu.GetInstructionSize(opcode);
            var offset = (ushort)(pc + baseAddress);
            var instruction = new byte[instructionSize];
            Array.Copy(programCode, pc, instruction, 0, instructionSize);
            sb.AppendLine(Cpu.Disassemble(offset, instruction));
            pc += instructionSize;
        }

        return sb.ToString();
    }

    public void Reset()
    {
        _frameflip = false;
        Cpu.Reset();
        Bus.Reset();
        Ppu.Reset();
    }

    /// <summary>
    ///     Plugs in the cartridge onto the current emulator console.
    /// </summary>
    /// <param name="cartridge">
    ///     The <see cref="Cartridge" /> instance that represents the cartridge needs to be installed to
    ///     the console.
    /// </param>
    /// <exception cref="NotSupportedException">Throws if the cartridge is currently not supported by the emulator.</exception>
    public void InstallCartridge(Cartridge cartridge)
    {
        InstalledCartridge = cartridge;
        var mapperRegistry = _registeredMapperTypes
                                 .FirstOrDefault(r => r.Number == InstalledCartridge.Mapper)
                             ??
                             throw new NotSupportedException(
                                 $"The mapper {InstalledCartridge.Mapper} is not supported.");
        _mapper = mapperRegistry.Factory(this);
    }

    /// <summary>
    ///     Opens a cartridge file and reset the emulator.
    /// </summary>
    /// <param name="cartridgeFileName">The name of the cartridge file, usually has the extension of .NES</param>
    public void Open(string cartridgeFileName)
    {
        InstallCartridge(new Cartridge(cartridgeFileName));
        Reset();
    }

    public void RemoveCartridge()
    {
        InstalledCartridge = null;
        _mapper = null;
    }

    /// <summary>
    ///     Runs the emulator which executes the CPU, PPU and memory mappers step by step.
    /// </summary>
    public void Run()
    {
        _stop = false;
        var stopwatch = new Stopwatch();
        while (!_stop)
        {
            stopwatch.Restart();
            ExecuteStep();
            stopwatch.Stop();

            Thread.Sleep(Math.Max((int)(700.0 / 60.0 - stopwatch.ElapsedMilliseconds), 0));
        }
    }

    /// <summary>
    ///     Stops the running of the emulator.
    /// </summary>
    public void Stop()
    {
        _stop = true;
    }

    #endregion Public Methods
}